﻿//------------------------------------------------------------------------------
// Copyright © FRA & FV 2014
// All rights reserved
//------------------------------------------------------------------------------
// Binary Serialization Custom Tool
//
// SVN revision information:
//   $Revision: 98 $
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace FRAFV.Binary.Serialization
{
	[Serializable]
	public sealed class InvalidCodeException : Exception
	{
		public int Offset { get; private set; }

		public InvalidCodeException(string message, int pos)
			: base(message)
		{
			this.Offset = pos;
		}

		public InvalidCodeException(Exception inner, int pos)
			: base(inner.Message, inner)
		{
			this.Offset = pos;
		}
	}

	[System.Diagnostics.DebuggerDisplay("{Namespace,nq}.{Name,nq}")]
	public class ClassParser
	{
		const string argFormat = @"(Format\s*=\s*)?BinFormat\.(?<format>\w+)";
		const string argChecker = @"Condition\s*=\s*(?<checker>@?"")";
		const string argEncoding = @"Encoding\s*=\s*""(?<encoding>[\w\-\.]+)""";
		const string argLengthFormat = @"(LengthFormat\s*=\s*)?BinFormat\.(?<length_format>\w+)";
		const string argEncodingMethod = @"EncodingCustomMethod\s*=\s*(?<encoding_method>@?"")";
		const string argLengthMethod = @"LengthCustomMethod\s*=\s*(?<length_method>@?"")";
		const string argVirtual = @"MethodMode\s*=\s*BinMethodMode\.(?<mode>\w+)";
		const string argReadMethod = @"ReadMethod\s*=\s*(?<read>@?"")";
		const string argGetDataSizeMethod = @"GetDataSizeMethod\s*=\s*(?<size>@?"")";
		const string argWriteMethod = @"WriteMethod\s*=\s*(?<write>@?"")";
		const string argBinReader = @"BinaryReaderType\s*=\s*typeof\s*\(\s*(?<reader>[\w\.<>,]+)\s*\)";
		const string argBinWriter = @"BinaryWriterType\s*=\s*typeof\s*\(\s*(?<writer>[\w\.<>,]+)\s*\)";
		const string argCountFormat = @"(CountFormat\s*=\s*)?BinFormat\.(?<count_format>\w+)";
		const string argCountMethod = @"CountCustomMethod\s*=\s*(?<count_method>@?"")";
		const string argConditionMethod = @"ConditionCustomMethod\s*=\s*(?<cond_method>@?"")";
		const string argType = @"ItemType\s*=\s*typeof\s*\(\s*(?<type>[\w\.<>,]+)\s*\)";
		const string argCreateMethod = @"CreateCustomMethod\s*=\s*(?<create>@?"")";
		const string argItemOrder = @"(ItemOrder\s*=\s*)?BinItemOrder\.(?<item_order>\w+)";
		const string argReadItemMethod = @"ReadItemMethod\s*=\s*(?<readitem>@?"")";
		const string argWriteItemMethod = @"WriteItemMethod\s*=\s*(?<writeitem>@?"")";
		const string argGetItemSizeMethod = @"GetItemSizeMethod\s*=\s*(?<sizeitem>@?"")";
		const string argItemUsage = @"(ItemUsage\s*=\s*)?BinItemUsage\.(?<usage>\w+)";
		static Regex reBinDataArg = new Regex(@"\G\s*(" +
			argFormat + @"|" +
			argChecker + @"|" +
			argLengthFormat + @"|" +
			argLengthMethod + @")");
		static Regex reBinBlockArg = new Regex(@"\G\s*(" +
			argVirtual + @"|" +
			argEncoding + @"|" +
			argEncodingMethod + @"|" +
			argCountFormat + @"|" +
			argBinReader + @"|" +
			argBinWriter + @"|" +
			argReadMethod + @"|" +
			argGetDataSizeMethod + @"|" +
			argWriteMethod + @"|" +
			argItemOrder + @"|" +
			argReadItemMethod + @"|" +
			argGetItemSizeMethod + @"|" +
			argWriteItemMethod + @")");
		static Regex reBinCustomArg = new Regex(@"\G\s*(" +
			argReadMethod + @"|" +
			argGetDataSizeMethod + @"|" +
			argWriteMethod + @")");
		static Regex reBinArrayArg = new Regex(@"\G\s*(" +
			argCountFormat + @"|" +
			argCountMethod + @"|" +
			argConditionMethod + @")");
		static Regex reBinArrayItemArg = new Regex(@"\G\s*(" +
			argType + @"|" +
			argCreateMethod + @")");
		static Regex reBinItemArg = new Regex(@"\G\s*(" +
			argItemUsage + @"|" +
			argType + @"|" +
			argChecker + @")");
		static Regex reBinArgEnd = new Regex(@"\G\s*(?<end>[,\)])");
		static Regex reBinAttr = new Regex(@"\G\s*(?<type>(BinData|BinArray|BinArrayItem|BinCustom|BinItem|BinBlock))(Attribute)?\s*(?<end>[\(\]])", RegexOptions.Multiline);
		static Regex reBinAttrEnd = new Regex(@"\G\s*(?<end>[,\]])");
		static Regex reProperty = new Regex(@"((public|private|protected|internal|static)\s+)*(?<type>\w[\w\-\.]+(<[\w\-\.<>\s,\[\]]+>)?(\[\])?)\s+(?<name>\w+)\s*(?<end>[{;=])", RegexOptions.Multiline);
		static Regex reClass = new Regex(@"(?<access>(public|private|protected|internal|abstract|sealed)\s+)*partial\s+class\s+(?<name>\w+)[^\:{]*((?<derive>\:)([^{]*(?<serializable>IBinSerializable)|[^{]*IBinEnumerable<(?<enumerable>.+)>)*)?", RegexOptions.Multiline);
		static Regex reNamespace = new Regex(@"namespace\s+(?<namespace>[\w\.]+)", RegexOptions.Multiline);
		static Regex reBinArrayType = new Regex(@"^((?<array_item>.+)\[\]|[\w\-\.]+<(?<collection_item>.+)>)$");

		[System.Diagnostics.DebuggerDisplay("{Type,nq} {Name,nq}")]
		public sealed class ClassProperty
		{
			public int Offset { get; private set; }
			public string Type { get; private set; }
			public string Name { get; private set; }
			public BinDataAttribute[] BinData { get; private set; }
			public BinCustomAttribute BinCustom { get; private set; }
			public BinArrayAttribute BinArray { get; private set; }
			public BinArrayItemAttribute BinArrayItem { get; private set; }
			public BinItemAttribute[] BinItem { get; private set; }
			public string ArrayType { get; private set; }
			public bool IsCollection { get; private set; }

			public ClassProperty(int pos, string type, string name, Attribute[] binAttr)
			{
				this.Offset = pos;
				this.Type = type;
				this.Name = name;
				this.BinData = binAttr.OfType<BinDataAttribute>().ToArray();
				this.BinCustom = binAttr.OfType<BinCustomAttribute>().FirstOrDefault();
				this.BinItem = binAttr.OfType<BinItemAttribute>().ToArray();
			}

			public ClassProperty(int pos, string type, string name, Attribute[] binAttr,
				BinArrayAttribute binArray, string propType, bool isCollection)
				: this(pos, type, name, binAttr)
			{
				this.BinArray = binArray;
				this.IsCollection = isCollection;
				this.ArrayType = propType;
				this.BinArrayItem = binAttr.OfType<BinArrayItemAttribute>().FirstOrDefault();
			}
		}

		internal class BinBlockBuilder : BinBlockAttribute
		{
			public string BinaryReaderTypeName { get; set; }
			public string BinaryWriterTypeName { get; set; }

			public BinBlockBuilder(BinBlockAttribute parent)
				: base(parent)
			{
				var binBlock = parent as BinBlockBuilder;
				if (binBlock != null)
				{
					this.BinaryReaderTypeName = binBlock.BinaryReaderTypeName;
					this.BinaryWriterTypeName = binBlock.BinaryWriterTypeName;
				}
				else
				{
					if (parent.BinaryReaderType != null) this.BinaryReaderTypeName = parent.BinaryReaderType.FullName;
					if (parent.BinaryWriterType != null) this.BinaryWriterTypeName = parent.BinaryWriterType.FullName;
				}
			}

			public BinBlockBuilder() { }

			public override string ToString()
			{
				string str = base.ToString();
				int k = str.IndexOf('(');
				var args = new List<string>();
				if (str.Length > k + 3)
					args.Add(str.Substring(k + 1, str.Length - k - 3));
				if (this.BinaryReaderTypeName != null)
					args.Add("BinaryReaderType=typeof(" + this.BinaryReaderTypeName + ")");
				if (this.BinaryWriterTypeName != null)
					args.Add("BinaryWriterType=typeof(" + this.BinaryWriterTypeName + ")");
				return str.Substring(0, k + 1) + String.Join(", ", args) + ")]";
			}
		}

		internal class BinArrayItemBuilder : BinArrayItemAttribute
		{
			public string ItemTypeName { get; set; }

			public override string ToString()
			{
				string str = base.ToString();
				int k = str.IndexOf('(');
				var args = new List<string>();
				if (str.Length > k + 3)
					args.Add(str.Substring(k + 1, str.Length - k - 3));
				if (this.ItemTypeName != null)
					args.Add("ItemType=typeof(" + this.ItemTypeName + ")");
				return str.Substring(0, k + 1) + String.Join(", ", args) + ")]";
			}
		}

		internal class BinItemBuilder : BinItemAttribute
		{
			public string ItemTypeName { get; set; }

			public override string ToString()
			{
				string str = base.ToString();
				int k = str.IndexOf('(');
				var args = new List<string>();
				if (str.Length > k + 3)
					args.Add(str.Substring(k + 1, str.Length - k - 3));
				if (this.ItemTypeName != null)
					args.Add("ItemType=typeof(" + this.ItemTypeName + ")");
				return str.Substring(0, k + 1) + String.Join(", ", args) + ")]";
			}
		}

		public int Offset { get; private set; }
		public string Namespace { get; private set; }
		public string Name { get; private set; }
		public bool IsBase { get; private set; }
		public bool IsSealed { get; private set; }
		public bool IBinSerializable { get; private set; }
		public string IEnumerableType { get; private set; }
		public BinBlockAttribute BinBlock { get; private set; }
		public IEnumerable<ClassProperty> Properties { get { return this.properties; } }
		public IEnumerable<ClassParser> Children { get { return this.children; } }

		private string content;
		private int end;
		private List<ClassProperty> properties = new List<ClassProperty>();
		private List<ClassParser> children = new List<ClassParser>();

		public ClassParser(string ns, string name, string content, int start, BinBlockAttribute binBlock)
		{
			this.Namespace = ns;
			this.Name = name;
			this.content = content;
			this.Offset = start;
			this.BinBlock = binBlock;
		}

		public static IEnumerable<ClassParser> GetPartials(string content, Action<string, uint, uint> error)
		{
			var root = new ClassParser(null, null, content, 0,
				new BinBlockBuilder
				{
					MethodMode = BinMethodMode.Override,
					LengthFormat = BinFormat.UInt8,
					CountFormat = BinFormat.UInt8,
					BinaryReaderTypeName = typeof(System.IO.BinaryReader).FullName,
					BinaryWriterTypeName = typeof(System.IO.BinaryWriter).FullName,
					ItemOrder = BinItemOrder.Sequence
				});
			try
			{
				root.Parse();
			}
			catch (InvalidCodeException ex)
			{
				uint row, column;
				ClassGenerator.ResolveErrorOffset(content, ex, out row, out column);
				error(ex.Message, row, column);
			}
			return root.children;
		}

		private bool ParseNamespace(int pos, string before)
		{
			MatchCollection matches;
			if (this.Name != null ||
				(matches = reNamespace.Matches(before)).Count == 0)
				return false;

			string ns = matches[matches.Count - 1].Groups["namespace"].Value;
			if (this.Namespace != null)
				ns = this.Namespace + "." + ns;
			this.Namespace = ns;
			return true;
		}

		private bool ParseChild(ref int pos, string before, BinBlockAttribute binBlock)
		{
			MatchCollection matches;
			if ((matches = reClass.Matches(before)).Count == 0)
				return false;

			var match = matches[matches.Count - 1];
			string name = match.Groups["name"].Value;
			//if (this.Name != null)
			//	name = this.Name + "." + name;
			var child = new ClassParser(this.Namespace, name, content, pos, binBlock)
			{
				IsBase = !match.Groups["derive"].Success,
				IsSealed = match.Groups["access"].Value.Contains("sealed"),
				IBinSerializable = match.Groups["serializable"].Success,
				IEnumerableType = match.Groups["enumerable"].Value.NullEmpty()
			};
			pos = child.Parse();
			if (child.properties.Count == 0 && child.children.Count == 0)
				return true; ;
			children.Add(child);
			//children.AddRange(child.children);
			return true;
		}

		private bool ParseComment(ref int pos)
		{
			if (content[pos + 1] == '/')
			{
				pos = content.IndexOfAny(new char[] { '\r', '\n' }, pos + 2);
				return true;
			}
			else if (content[pos + 1] == '*')
			{
				pos = content.IndexOf("*/", pos + 2);
				if (pos > 0) pos++;
				return true;
			}
			else
				return false;
		}

		private void ParseDirective(ref int pos)
		{
			pos = content.IndexOfAny(new char[] { '\r', '\n' }, pos + 1);
		}

		private void ParseCString(ref int pos)
		{
			char quote = content[pos];
			do
			{
				pos = content.IndexOf(quote, pos + 1);
			} while (pos > 0 && content[pos - 1] == '\\');
		}

		private void ParseVBString(ref int pos)
		{
			do
			{
				pos = content.IndexOf('"', pos + 1);
			} while (pos > 0 && pos < content.Length - 1 && content[pos + 1] == '"');
		}

		private string ParseAttribute(ref int pos)
		{
			var match = reBinAttr.Match(content, pos + 1);
			if (!match.Success) return null;
			if (match.Groups["end"].Value == "]")
				pos = match.Groups["end"].Index;
			else
				pos = match.Index + match.Length;
			return match.Groups["type"].Value;
		}

		private string ResolveArgValue(ref int pos, Group quote)
		{
			pos = quote.Index;
			switch (quote.Value)
			{
				case "@\"":
					pos++;
					ParseVBString(ref pos);
					if (pos < 0)
						throw new InvalidCodeException("Invalid string value", quote.Index);
					pos++;
					return content.Substring(quote.Index + 2, pos - quote.Index - 3)
						.Replace("\"\"", "\"");
				case "\"":
					ParseCString(ref pos);
					if (pos < 0)
						throw new InvalidCodeException("Invalid string value", quote.Index);
					pos++;
					//HttpUtility.JavaScriptStringEncode
					return content.Substring(quote.Index + 1, pos - quote.Index - 2)
						.Replace(@"\""", "\"");
				default:
					throw new InvalidCodeException("Invalid string value", pos);
			}
		}

		private void ParseBinArray(ref int pos, BinArrayAttribute binArray)
		{
			for (Match arg; (arg = reBinArrayArg.Match(content, pos)).Success; )
			{
				var mCountFormat = arg.Groups["count_format"];
				var mCountMethod = arg.Groups["count_method"];
				var mCondition = arg.Groups["cond_method"];
				pos += arg.Length;
				if (mCountFormat.Success) binArray.CountFormat = (BinFormat)Enum.Parse(typeof(BinFormat), mCountFormat.Value);
				if (mCountMethod.Success) binArray.CountCustomMethod = ResolveArgValue(ref pos, mCountMethod);
				if (mCondition.Success) binArray.ConditionCustomMethod = ResolveArgValue(ref pos, mCondition);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
			if (!binArray.ConditionCustomMethod.IsNullEmpty() &&
				(binArray.CountFormat != BinFormat.Auto ||
				 !binArray.CountCustomMethod.IsNullEmpty()))
				throw new InvalidCodeException("ConditionCustomMethod with other properties isn't allowed", pos);
			switch (binArray.CountFormat)
			{
				case BinFormat.Auto:
					if (binArray.CountCustomMethod.IsNullEmpty() && binArray.ConditionCustomMethod.IsNullEmpty())
						binArray.CountFormat = this.BinBlock.CountFormat;
					break;
				case BinFormat.Int8:
				case BinFormat.Int16:
				case BinFormat.Int24:
				case BinFormat.Int32:
				case BinFormat.Int64:
				case BinFormat.UInt8:
				case BinFormat.UInt16:
				case BinFormat.UInt24:
				case BinFormat.UInt32:
				case BinFormat.UInt64:
				case BinFormat.SynchSafeInt16:
				case BinFormat.SynchSafeInt32:
				case BinFormat.SynchSafeInt64:
					break;
				default:
					throw new InvalidCodeException("Unsupported count format " + binArray.CountFormat, pos);
			}
		}

		private void ParseBinArrayItem(ref int pos, BinArrayItemBuilder binArrayItem)
		{
			for (Match arg; (arg = reBinArrayItemArg.Match(content, pos)).Success; )
			{
				var mType = arg.Groups["type"];
				var mCreateMethod = arg.Groups["create"];
				pos += arg.Length;
				if (mType.Success) binArrayItem.ItemTypeName = mType.Value;
				if (mCreateMethod.Success) binArrayItem.CreateCustomMethod = ResolveArgValue(ref pos, mCreateMethod);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
		}

		private void ParseBinData(ref int pos, BinDataAttribute binData)
		{
			for (Match arg; (arg = reBinDataArg.Match(content, pos)).Success; )
			{
				var mFormat = arg.Groups["format"];
				var mChecker = arg.Groups["checker"];
				var mLengthFormat = arg.Groups["length_format"];
				var mLengthMethod = arg.Groups["length_method"];
				pos += arg.Length;
				if (mFormat.Success) binData.Format = (BinFormat)Enum.Parse(typeof(BinFormat), mFormat.Value);
				if (mChecker.Success) binData.Condition = ResolveArgValue(ref pos, mChecker);
				if (mLengthFormat.Success) binData.LengthFormat = (BinFormat)Enum.Parse(typeof(BinFormat), mLengthFormat.Value);
				if (mLengthMethod.Success) binData.LengthCustomMethod = ResolveArgValue(ref pos, mLengthMethod);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
			switch (binData.LengthFormat)
			{
				case BinFormat.Auto:
					if (binData.LengthCustomMethod.IsNullEmpty())
						binData.LengthFormat = this.BinBlock.LengthFormat;
					break;
				case BinFormat.Int8:
				case BinFormat.Int16:
				case BinFormat.Int24:
				case BinFormat.Int32:
				case BinFormat.Int64:
				case BinFormat.UInt8:
				case BinFormat.UInt16:
				case BinFormat.UInt24:
				case BinFormat.UInt32:
				case BinFormat.UInt64:
				case BinFormat.SynchSafeInt16:
				case BinFormat.SynchSafeInt32:
				case BinFormat.SynchSafeInt64:
					break;
				default:
					throw new InvalidCodeException("Unsupported length format " + binData.LengthFormat, pos);
			}
		}

		private void ParseBinCustom(ref int pos, BinCustomAttribute binCustom)
		{
			for (Match arg; (arg = reBinCustomArg.Match(content, pos)).Success; )
			{
				var mRead = arg.Groups["read"];
				var mSize = arg.Groups["size"];
				var mWrite = arg.Groups["write"];
				pos += arg.Length;
				if (mRead.Success) binCustom.ReadMethod = ResolveArgValue(ref pos, mRead);
				if (mSize.Success) binCustom.GetDataSizeMethod = ResolveArgValue(ref pos, mSize);
				if (mWrite.Success) binCustom.WriteMethod = ResolveArgValue(ref pos, mWrite);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
		}

		private void ParseBinItem(ref int pos, BinItemBuilder binItem)
		{
			for (Match arg; (arg = reBinItemArg.Match(content, pos)).Success; )
			{
				var mUsage = arg.Groups["usage"];
				var mType = arg.Groups["type"];
				var mChecker = arg.Groups["checker"];
				pos += arg.Length;
				if (mUsage.Success) binItem.ItemUsage = (BinItemUsage)Enum.Parse(typeof(BinItemUsage), mUsage.Value);
				if (mType.Success) binItem.ItemTypeName = mType.Value;
				if (mChecker.Success) binItem.Condition = ResolveArgValue(ref pos, mChecker);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
		}

		private void ParseNextAttribute(ref int pos, List<Attribute> binAttr)
		{
			var m = content[pos] == '[' ? null : reBinAttrEnd.Match(content, pos);
			while (m == null || m.Success && m.Groups["end"].Value == ",")
			{
				switch (ParseAttribute(ref pos))
				{
					case "BinData":
						if (binAttr == null || binAttr.OfType<BinArrayItemAttribute>().Any() || binAttr.OfType<BinItemAttribute>().Any())
							throw new InvalidCodeException("Unexpected attribute BinData", pos);
						if (binAttr.OfType<BinDataAttribute>().Any(attr => attr.Condition.IsNullEmpty()))
							throw new InvalidCodeException("Only last BinData attribute may not have Condition", pos);
						var last = new BinDataAttribute();
						binAttr.Add(last);
						ParseBinData(ref pos, last);
						break;
					case "BinArrayItem":
						if (binAttr == null)
							throw new InvalidCodeException("Unexpected attribute BinArrayItem", pos);
						if (binAttr.Count > 0)
							throw new InvalidCodeException("Only one BinArrayItem attribute is allowed", pos);
						var arrayitem = new BinArrayItemBuilder();
						binAttr.Add(arrayitem);
						ParseBinArrayItem(ref pos, arrayitem);
						break;
					case "BinItem":
						if (binAttr == null || binAttr.OfType<BinItemAttribute>().Count() != binAttr.Count())
							throw new InvalidCodeException("Unexpected attribute BinItem", pos);
						var item = new BinItemBuilder();
						ParseBinItem(ref pos, item);
						switch (item.ItemUsage)
						{
							case BinItemUsage.Optional:
								if (binAttr.OfType<BinItemAttribute>().Any(attr => attr.ItemUsage == BinItemUsage.Repeatable))
									throw new InvalidCodeException("Unexpected attribute BinItem with Optional usage", pos);
								break;
							case BinItemUsage.Required:
								if (binAttr.OfType<BinItemAttribute>().Any(attr => attr.ItemUsage == BinItemUsage.Repeatable))
									throw new InvalidCodeException("Unexpected attribute BinItem with Required usage", pos);
								if (binAttr.OfType<BinItemAttribute>().Any(attr => attr.ItemUsage == BinItemUsage.Required))
									throw new InvalidCodeException("Only one BinItem attribute with Required usage allowed", pos);
								break;
							case BinItemUsage.Repeatable:
								if (binAttr.OfType<BinItemAttribute>().Any(attr => attr.ItemUsage == BinItemUsage.Optional || attr.ItemUsage == BinItemUsage.Required))
									throw new InvalidCodeException("Unexpected attribute BinItem with Repeatable usage", pos);
								break;
						}
						binAttr.Add(item);
						break;
					case null:
						return;
					default:
						throw new InvalidCodeException("Only single attribute is allowed", pos);
				}
				m = reBinAttrEnd.Match(content, pos);
			}
		}

		private int FindPropertyBound(ref int start, List<Attribute> binAttr)
		{
			ParseNextAttribute(ref start, binAttr);
			int pos = start;
			bool found;
			while (true)
			{
				pos = content.IndexOfAny(new char[] { '{', ';', '/', '"', '@', '[', '#' }, pos);
				if (pos < 0 || pos >= content.Length - 1)
					throw new InvalidCodeException("Class property not found", pos);
				found = false;
				switch (content[pos])
				{
					case '{':
					case ';':
						return pos;
					case '/':
						found = reProperty.IsMatch(content.Substring(start, pos - start + 1));
						ParseComment(ref pos);
						break;
					case '"':
					case '\'':
						ParseCString(ref pos);
						break;
					case '@':
						if (content[pos + 1] != '"') break;
						pos++;
						ParseVBString(ref pos);
						break;
					case '[':
						if (content[pos + 1] == ']') //as part of property type
						{
							pos += 2;
							continue;
						}
						ParseNextAttribute(ref pos, binAttr);
						break;
					case '#':
						found = reProperty.IsMatch(content.Substring(start, pos - start + 1));
						ParseDirective(ref pos);
						break;
				}
				if (pos < 0)
					throw new InvalidCodeException("Class property not found", pos);
				pos++;
				if (!found) start = pos;
			}
		}

		private void ParseProperty(ref int pos, int pos2, BinArrayAttribute binArray, params Attribute[] binAttr)
		{
			var match = reProperty.Match(content, pos, pos2 - pos + 1);
			if (!match.Success)
				throw new InvalidCodeException("Class property not found", pos);
			pos = match.Index;
			var mType = match.Groups["type"];
			var mName = match.Groups["name"];
			var binItems = binAttr.OfType<BinItemAttribute>();
			if (binItems.Any() && BinBlock.ReadItemMethod.IsNullEmpty()) //simple items
			{
				if ((binItems.FirstOrDefault(attr => attr.Condition.IsNullEmpty()) ?? binItems.LastOrDefault()) != binItems.LastOrDefault())
					throw new InvalidCodeException("Without item custom resolver (BinBlock.ReadItemMethod) only last BinItem attribute may not have Condition", pos);
				if (BinBlock.ItemOrder == BinItemOrder.Any)
					throw new InvalidCodeException("Without item custom resolver (BinBlock.ReadItemMethod) Any order kind isn't allowed", pos);
				if (BinBlock.ItemOrder == BinItemOrder.Choice)
				{
					if (binItems.Any(attr => attr.Condition.IsNullEmpty()))
						throw new InvalidCodeException("Without item custom resolver (BinBlock.ReadItemMethod) Condition is required in Choice order kind", pos);
					if (IEnumerableType.IsNullEmpty())
						throw new InvalidCodeException("Without item custom resolver (BinBlock.ReadItemMethod) IEnumerable<> impelementation is required in Choice order kind", pos);
				}
			}
			if (BinBlock.ItemOrder == BinItemOrder.Choice)
			{
				if (binItems.Any(attr => attr.ItemUsage == BinItemUsage.Required))
					throw new InvalidCodeException("Required items aren't allowed in Choice order kind", pos);
				if (binItems.Any(attr => attr.ItemUsage == BinItemUsage.Repeatable))
					throw new InvalidCodeException("Repeatable items aren't allowed in Choice order kind", pos);
			}
			if (binItems.Any() && binItems.FirstOrDefault().ItemUsage == BinItemUsage.Repeatable)
			{
				var m2 = reBinArrayType.Match(mType.Value);
				var binItem = binAttr.Length != 1 ? null : binAttr[0] as BinItemAttribute;
				string itemType = binItem.ResolveItemType();
				if (m2.Success)
				{
					var mColType = m2.Groups["collection_item"];
					if (!mColType.Success)
						throw new InvalidCodeException("Array item type isn't allowed", pos);
					if (itemType.IsNullEmpty())
					{
						itemType = mColType.Value;
					}
				}
				else
				{
					if (itemType.IsNullEmpty())
						throw new InvalidCodeException("It isn't possible to determinate collection item type", pos);
				}
				properties.Add(new ClassProperty(pos, itemType, mName.Value, binAttr, null, mType.Value, true));
			}
			else if (binArray == null)
			{
				properties.Add(new ClassProperty(pos, mType.Value, mName.Value, binAttr));
			}
			else
			{
				if (binAttr.Length == 0)
					binAttr = new Attribute[] { new BinArrayItemBuilder() };
				var m2 = reBinArrayType.Match(mType.Value);
				var binArrayItem = binAttr.Length != 1 ? null : binAttr[0] as BinArrayItemAttribute;
				var binData = binAttr.Length != 1 ? null : binAttr[0] as BinDataAttribute;
				string itemType = binArrayItem.ResolveItemType();
				int num;
				bool numcount = !binArray.CountCustomMethod.IsNullEmpty() &&
					int.TryParse(binArray.CountCustomMethod, NumberStyles.None, null, out num);
				if (m2.Success)
				{
					var mArrayType = m2.Groups["array_item"];
					var mColType = m2.Groups["collection_item"];
					bool col = mColType.Success && !numcount || !binArray.ConditionCustomMethod.IsNullEmpty();
					if (itemType.IsNullEmpty())
					{
						if (mArrayType.Success)
							itemType = mArrayType.Value;
						else //if (mColType.Success)
							itemType = mColType.Value;
					}
					properties.Add(new ClassProperty(pos, itemType, mName.Value, binAttr,
						binArray, mType.Value, mColType.Success));
				}
				else
				{
					if ((binData == null || binData.Format == BinFormat.Auto) && itemType.IsNullEmpty())
						throw new InvalidCodeException("It isn't possible to determinate array item type", pos);
					bool array = binData != null && binArray.ConditionCustomMethod.IsNullEmpty() &&
						(numcount || !binArray.CountCustomMethod.IsNullEmpty() && binData.Format == BinFormat.Auto);
					if (itemType.IsNullEmpty())
					{
						itemType = binData.Format.AsType().FullName;
					}
					properties.Add(new ClassProperty(pos, itemType,
						mName.Value, binAttr, binArray, mType.Value, !array));
				}
			}
			pos = match.Groups["end"].Index - 1;
		}

		private void ParseBinBlock(ref int pos, BinBlockBuilder binBlock)
		{
			for (Match arg; (arg = reBinBlockArg.Match(content, pos)).Success; )
			{
				var mVirtual = arg.Groups["mode"];
				var mEncoding = arg.Groups["encoding"];
				var mEncodingMethod = arg.Groups["encoding_method"];
				var mLengthFormat = arg.Groups["length_format"];
				var mBinReader = arg.Groups["reader"];
				var mBinWriter = arg.Groups["writer"];
				var mRead = arg.Groups["read"];
				var mSize = arg.Groups["size"];
				var mWrite = arg.Groups["write"];
				var mItemOrder = arg.Groups["item_order"];
				var mChecker = arg.Groups["any_cond_method"];
				var mReadItem = arg.Groups["readitem"];
				var mSizeItem = arg.Groups["sizeitem"];
				var mWriteItem = arg.Groups["writeitem"];
				pos += arg.Length;
				if (mVirtual.Success) binBlock.MethodMode = (BinMethodMode)Enum.Parse(typeof(BinMethodMode), mVirtual.Value);
				if (mEncoding.Success) binBlock.Encoding = mEncoding.Value;
				if (mEncodingMethod.Success) binBlock.EncodingCustomMethod = ResolveArgValue(ref pos, mEncodingMethod);
				if (mLengthFormat.Success) binBlock.LengthFormat = (BinFormat)Enum.Parse(typeof(BinFormat), mLengthFormat.Value);
				if (mBinReader.Success) binBlock.BinaryReaderTypeName = mBinReader.Value;
				if (mBinWriter.Success) binBlock.BinaryWriterTypeName = mBinWriter.Value;
				if (mRead.Success) binBlock.ReadMethod = ResolveArgValue(ref pos, mRead);
				if (mSize.Success) binBlock.GetDataSizeMethod = ResolveArgValue(ref pos, mSize);
				if (mWrite.Success) binBlock.WriteMethod = ResolveArgValue(ref pos, mWrite);
				if (mItemOrder.Success) binBlock.ItemOrder = (BinItemOrder)Enum.Parse(typeof(BinItemOrder), mItemOrder.Value);
				if (mReadItem.Success) binBlock.ReadItemMethod = ResolveArgValue(ref pos, mReadItem);
				if (mSizeItem.Success) binBlock.GetItemSizeMethod = ResolveArgValue(ref pos, mSizeItem);
				if (mWriteItem.Success) binBlock.WriteItemMethod = ResolveArgValue(ref pos, mWriteItem);
				arg = reBinArgEnd.Match(content, pos);
				if (arg.Success) pos += arg.Length;
				if (arg.Groups["end"].Value != ",") break;
			}
		}

		private int Parse()
		{
			int level = 0, last = Offset, pos2;
			var binBlock = new BinBlockBuilder(this.BinBlock);
			for (int pos = Offset; true; pos++)
			{
				pos = content.IndexOfAny(new char[] { '{', '}', '/', '"', '@', '[', '#' }, pos);
				if (pos < 0 || pos >= content.Length - 1)
					throw new InvalidCodeException("Class end not found", pos);
				switch (content[pos])
				{
					case '{':
						string before = content.Substring(last, pos - last);
						last = pos + 1;
						if (ParseNamespace(pos, before) ||
							!ParseChild(ref pos, before, binBlock))
							level++;
						binBlock = new BinBlockBuilder(this.BinBlock);
						break;
					case '}':
						if (level == 0)
							throw new InvalidCodeException("Unexpected close '}'", pos);
						last = pos + 1;
						if (level == 1)
						{
							return this.end = pos;
						}
						level--;
						binBlock = new BinBlockBuilder(this.BinBlock);
						break;
					case '/':
						ParseComment(ref pos);
						break;
					case '"':
					case '\'':
						ParseCString(ref pos);
						break;
					case '@':
						if (content[pos + 1] != '"') break;
						pos++;
						ParseVBString(ref pos);
						break;
					case '#':
						ParseDirective(ref pos);
						break;
					case '[':
						var binData = new List<Attribute>();
						switch(ParseAttribute(ref pos))
						{
							case "BinData":
								binData.Add(new BinDataAttribute());
								ParseBinData(ref pos, (BinDataAttribute)binData[0]);
								pos2 = FindPropertyBound(ref pos, binData);
								ParseProperty(ref pos, pos2, null, binData.ToArray());
								break;
							case "BinArray":
								var binArray = new BinArrayAttribute();
								ParseBinArray(ref pos, binArray);
								pos2 = FindPropertyBound(ref pos, binData);
								ParseProperty(ref pos, pos2, binArray, binData.ToArray());
								break;
							case "BinCustom":
								var binCustom = new BinCustomAttribute();
								ParseBinCustom(ref pos, binCustom);
								pos2 = FindPropertyBound(ref pos, null);
								ParseProperty(ref pos, pos2, null, binCustom);
								break;
							case "BinItem":
								binData.Add(new BinItemBuilder());
								ParseBinItem(ref pos, (BinItemBuilder)binData[0]);
								pos2 = FindPropertyBound(ref pos, binData);
								ParseProperty(ref pos, pos2, null, binData.ToArray());
								break;
							case "BinBlock":
								ParseBinBlock(ref pos, binBlock);
								break;
							case null:
								break;
							case "BinArrayItem":
								throw new InvalidCodeException("Unexpected attribute BinArrayItem; attribute BinArray not found", pos);
							default:
								throw new InvalidCodeException("Unknown attribute", pos);
						}
						break;
				}
				if (pos < 0)
					throw new InvalidCodeException("Class end not found", pos);
			}
		}
	}
}
